에러 처리·API 설계 실습 — strtol my_atoi·snprintf_m·sum_file·conformant 배열

NOTE

C 기초 강의의 에러 처리(13-01 ~ 13-06)와 API 설계(14-01 ~ 14-04) 실습에서 만난 오류 패턴을 정리하고, my_atoi, snprintf_m, sum_file, **conformant 배열 파라미터를 쓰는 improved_strncpy/strncat**을 직접 구현하며 겪은 버그를 기록한다.

환경은 Docker Desktop의 Linux 컨테이너(c_app_part1), gcc -Wall -Wextra.

IMPORTANT

코드는 컨테이너에서 컴파일·실행해 확인했고, 설명은 AI(Claude)와 함께 코드 리뷰하며 정리했다.

TIP

📂 원본 소스 (GitHub)

TIP

📂 환경

  • 접속: docker exec -it c_app_part1 /bin/bash
  • 컨테이너의 gcc가 오래돼서(4.8) -Wvla-parameter 같은 최신 경고 옵션은 쓸 수 없다.
  • 이 노트의 규칙은 종합본의 표와 체크리스트에도 반영돼 있다.

📌 한눈에 보기

실습문제처방
13-01printf 여러 번 뒤에 errno로 출력 오류 판단성공한 호출도 errno를 바꿀 수 있다 → 스트림 오류는 ferror(stdout)
13-02sprintf 반환값(쓴 길이)을 누적해 위치로 사용, 오류·오버플로 검사 없음in-band 에러 지시자를 피한다 → 에러는 반환값, 값은 인자로. snprintf 사용
13-03abort()로 종료 → atexit·정리 미실행, 파일 버퍼가 써질지는 구현에 따라 다름종료는 return/exit, fclose는 FILE *에
13-05atoi는 에러를 알려주지 않음strtol + endptr + errno
13-06strtoul 전에 errno를 0으로 안 만듦호출 직전 errno = 0, 호출 직후 바로 검사
14-01인자(FILE *) 검사 없음, 읽기 루프가 끝난 이유를 구분 안 함진입부에서 NULL 검사, 끝난 뒤 ferror / feof 구분
14-02복사 함수에 대상 크기가 없고 반환값도 없음dst_size 인자 + off-by-one 주의
14-03배열 인자의 크기를 알려 줄 방법이 없음conformant 배열 파라미터(크기를 앞에 선언)
14-04non-blocking write가 EAGAIN일 때 대기 없이 재시도재시도 사이에 시간을 주거나 select/poll로 대기

1. 에러 처리 — 13-01 ~ 13-06

errno는 믿을 수 없는 지점이 있다 (13-01)

요약

  • 문제: printf 여러 번 뒤 errno != 0으로 출력 오류를 판단 (13-01)
  • 원인: 성공한 호출도 errno를 바꾸고, 어느 호출의 값인지 알 수 없음
  • 처방: 스트림 오류는 ferror(stdout), 쓰기 전 errno = 0 + 호출 직후에만 읽기. 다시 쓰려면 clearerr()

in-band 에러 지시자를 피한다 (13-02)

요약

  • 문제: count += sprintf(...)에서 오류 -1이 위치에 더해져 앞쪽 메모리를 덮음 (13-02)
  • 원인: sprintf는 한 값으로 쓴 길이와 에러를 같이 알리고, 버퍼 크기를 모름
  • 처방: 에러는 반환값, 쓴 길이는 인자로 돌려주는 함수 (§3 snprintf_m)

종료 방법 (13-03)

요약

  • 문제: abort()로 종료하면 정리가 안 되고 버퍼가 써질지가 구현에 달림 (13-03)
  • 원인: abort/_Exit는 atexit를 건너뜀. close(f)는 FILE *에 쓴 오류
  • 처방: 종료는 return/exit, FILE *에는 fclose

atoi 대신 strtol (13-05, 13-06)

요약

  • 문제: atoi가 abc·범위 초과를 알려 주지 않음 (13-05, 13-06)
  • 원인: 에러 정보가 반환값에 없음
  • 처방: strtol/strtoul + endptr + errno == ERANGE, 호출 직전 errno = 0

2. 실습 1 — my_atoi (예외를 반환하는 atoi)

처음 코드의 문제


3. 실습 2 — snprintf_m (in-band 에러 제거)

처음 코드의 문제


4. 실습 3 — sum_file (줄 단위 합계와 에러 코드)

요구사항: 파일의 각 줄에 10진수 정수가 하나씩 있다. 반환값은 에러 코드로만 쓰고, 합계는 sum 인자로, 에러가 난 줄은 err_line으로 전달한다. 변환은 strtol, 합계가 long 범위를 넘으면 SUM_ERANGE.

EOF와 읽기 오류는 ferror()로 구분하고, 에러 시 sum을 바꾸지 않으며 어떤 경우에도 파일을 닫고 반환한다. 함수 안에서 메시지 출력·exit()·abort() 금지.

처음 구현의 실제 동작

요약

  • 문제: sum_file이 10, abc, 30을 40으로, 4000000000을 음수로 처리
  • 원인: atoi류 사용, int 잘림, 오버플로, __LINE__을 에러 줄로 사용
  • 처방: strtol + 끝 문자 검사, 더하기 전 한계 검사, 로컬 total에 모았다 성공 때만 *sum 갱신, 파일 줄번호, ferror로 읽기 오류 구분
  • 결과: CRLF·음수·빈 파일·1100자 줄·디렉토리 포함 테스트 통과

고친 구조


5. API 설계 — 14-01 ~ 14-04

인자 검사와 읽기 종료 (14-01)

요약

  • 문제: myFile이 NULL이어도 검사 없이 fgets (14-01)
  • 원인: 진입부 검사 없음, 루프가 끝난 이유(EOF/오류)를 구분하지 않음
  • 처방: 진입부에서 선제 검사, 루프 뒤 ferror/feof로 구분

크기 없는 복사 함수 (14-02)

요약

  • 문제: copy(dst, src)에 대상 크기와 return이 없고, 크기를 받는 버전도 1바이트 넘침 (14-02)
  • 원인: dstend = dst + dst_size 후 *dst = '\0'이 dst[dst_size]에 씀 (교육자료 수정안도 같음)
  • 처방: dstend = dst + dst_size - 1, 크기는 원소 개수로, 꽉 찬 입력 경계값으로 확인

conformant 배열 파라미터 (14-03)

요약

  • 문제: 배열 인자의 크기를 전달할 방법이 없음 (14-03)
  • 원인: 배열 매개변수는 포인터라 sizeof(p)가 포인터 크기
  • 처방: void f(size_t n, char p[n])처럼 크기를 앞에 선언 (뒤에 두면 GNU 확장). NULL·크기 검사는 별도로

non-blocking 재시도 (14-04)

요약

  • 문제: EAGAIN에서 대기 없이 곧바로 재시도해 횟수만 소진 (14-04)
  • 원인: 지금은 쓸 수 없다는 신호를 즉시 재시도로 처리
  • 처방: 재시도 사이에 시간을 주거나 select/poll로 대기, write_len을 누적해 partial write 처리

6. 실습 4 — conformant 배열로 만든 improved_strncpy/strncat

크기를 앞에 선언한 conformant 파라미터로 strncpy/strncat을 안전하게 만든 함수.

char* improved_strncpy(size_t destcount, char dest[destcount], size_t srccount, const char src[srccount], size_t n);
char* improved_strncat(size_t destcount, char dest[destcount], size_t srccount, const char src[srccount], size_t n);

규약 (요구사항 문서가 없어 직접 정함)

처음 코드의 문제

요약

  • 문제: improved_strncpy/strncat이 n을 안 쓰고, strncpy 사용, 널 문자 자리 누락, size_t 뺄셈
  • 원인: dest가 널 종료되지 않았거나 영역이 겹칠 때 검사 없음
  • 처방: memchr로 dest 확인, memcpy 후 dest[len] = '\0', len >= destcount - dlen 검사, 겹치면 EINVAL, 에러는 NULL+errno
  • 결과: 8칸에 1234567은 성공, 12345678은 ERANGE(dest 불변)

📝 교육자료 추가 과제 (14-6) — TBCD 변환 API, 아직 구현하지 않음


✅ 정리 — 에러 처리·API 점검 체크리스트